#charset "us-ascii"
#pragma once
/*
* Remap 'cmd' to another 'cmd'
*
* Accepts in the source remap a string that has "|" and "(" for grouping.
* It does NOT support "*" or "?" or any wildcards nor does it accept words
* that use special characters outside of apostrophe (') or comma ','.
*
* Several forms of the mapping occur:
* 1. Provide the actual text for the new command
* OR 2. Do a normal doInstead command with the appropriate values [via execute()]
* 3. Or put out a message with "" that ends the command [via execute()]
*
*
* Contributed by Mitchell Mlinar
* Copyright (c) 2025
*
* Licensed using MIT defintion
*
*
* v1.00: 14 Oct 2025
* v1.02: 15 Oct 2025 (thanks to advice/feedback from Eric Eve)
* v1.03: 16 Oct 2025 (thanks to additional advice/feedback from Eric Eve)
*
*/
#include "advlite.h"
#include <lookup.h>
/*
* The object put into your code
*/
/*
* RemapCmd is similar a Doer in that it remaps one command into another command, but
* there the differences end. What are the differences?
*
* RemapCmd parses up the cmd and does not make any attempt to map them to existing
* objects in the game. Rather, it builds all the possible variations (by the use of
* "|", "(" and ")" operators) and then matches that against the user input FIRST before
* any other routines get a whack at it. You can also provide two or more disjoint
* phrases that are separated by semi-colon ";" to provide additional phrases that
* match.
*
* If a match is found against a RemapCmd, it will then act on it. There are three
* different ways top operate on it.
*
* - provide a complete text phrase replacement (remappedCmd, or use template)
* - if no replacement text phrase is provided, the execute() routine is run for that
* object; within that execute routine, you have two possibilities
* * emit some messages to the console via say() or "..."
* * use doInstead(action[,dobj[,iobj[,aobj]]]) within to execute that resolved
* command instead
* - NOTE: If text phrase is present, execute() will NOT be run!
*
* Note that RemapCmd does not accept wildcards or punctuation of any sort other than
* "," and "'" -- and ";" to separate distinct input phrases.
*
*/
class RemapCmd: object
/* The command text to be recognized (see above) */
cmd = ''
/*
* The remapped (replacement) text (if provided). This should be in the form of a command the
* parser can parse and exectute
*/
remappedCmd = nil
/*
* If remapped command is NOT provided, game code should override this method to do
* something (or display some text).
*/
execute() {}
/*
* where this can happen (nil if everywhere); can be Room/Region or list of Rooms and/or
* Regions.
*/
where = nil
/* An expression defining under what circumstances this RemapCmd is matched. */
when = true
/*
* A scene that must be happening, or list one of scenes of which must be happening for
* this to happen (nil if no scene is required)
*/
during = nil
/*
* By default, you generally want any non-system command to take a turn.
* However, there may also be other circumstances where a turn should not be
* consumed. Change to 0 if we want execute() to NOT count as a turn.
* This property is ignored if doInstead(...) is called within execute()
*/
turnsTaken = 1
//////////////////////////////////////////
// Internals
//
/* Execute our custom method and then our turn sequence. For internal use only. */
execute_()
{
"<.p0>";
execute();
turnSequence();
}
/*
* If doInstead has been used in our execute() method, then call the standard turn sequence
* routine to execute any Events and update the turn counter. For internal use only.
*/
turnSequence() {
if(doInsteadItems == nil)
delegated Action;
}
/*
* A list containing the actoin and objects defined by a call to doInstead in our execute
* routine, or nil if doInstead() wasn't used. For internal use only
*/
doInsteadItems = nil
/*
* Populate our doInsteadItems from any call to doInstead() in our execute() routine. For
* internal use only.
*/
doInstead(action,[args]) {
/* get the optional items */
local dobj = args.element(1);
local iobj = args.element(2);
local aobj = args.element(3);
local deftext = '';
local len = args.length();
if(doInsteadItems != nil) {
"ERROR in doInstead(...) for remapCmd:\nYou cannot have more than one
doInstead\b";
abort;
}
if(action == nil)
deftext += 'action is undefined!\n';
if(dobj == nil && len > 0)
deftext += 'direct object is set, but not defined/understood!\n';
if(iobj == nil && len > 1)
deftext += 'indirect object is set, but not defined/understood!\n';
if(aobj == nil && len > 2)
deftext += 'auxiliary object is set, but not defined/understood!\n';
if(deftext != '') {
"ERROR in doInstead(...) for remapCmd:\n<<deftext>>\b";
abort;
}
doInsteadItems = [action,dobj,iobj,aobj];
}
/* The strings that are the command(s) to match. For internal use only. */
cmdTerms = []
/* The hash for these strings (faster compare later). For internal use only. */
cmdTermHash = []
;
/* ------------------------------------------------------------------------ */
/*
* This gathers up all of the RemapCmd items and gets them ready for execution in the
* actual game
*
* This is persistent and there should only be one of these
*
*/
remapCmdDicts: PreinitObject
/* Lookup table with hash and then list of objects that match it */
remapTbl = nil
/* The pre-init execution routine */
execute() {
remapTbl = new LookupTable(50,50);
local scmp = new StringComparator(nil,true,nil); // case-sensitive as lower anyway
for(local obj = firstObj(RemapCmd); obj != nil; obj = nextObj(obj,RemapCmd)) {
// process the command
local res, toks;
local lst = [];
foreach(local str in obj.cmd.split(';')) {
try {
toks = remapCmdTokenizer.tokenize(str);
match = remapCmdGrammar.parseTokens(toks,nil);
if(match.length > 0) {
res = match[1].lstval();
lst = remapCmdGrammarOr(lst,res);
} else
throw new EvalToksError(toks);
}
catch (TokErrorNoMatch err)
{
"Unrecognized punctuation: <<err.remainingStr_.substr(1, 1)>>";
break;
}
catch (EvalToksError err)
{
"Command phrase cannot be processed: <<str>>";
break;
}
}
// have the parsed up items -- now join them up into individual possible commands
foreach(toks in lst) {
// always only allow ONE space between words
res = toks.join(' ').findReplace(R'<Space><Space>+',' ',ReplaceAll);
res = res.findReplace(R'(^<Space>+)|(<Space>+$)','',ReplaceAll);
obj.cmdTerms = obj.cmdTerms.append(res);
res = scmp.calcHash(res);
obj.cmdTermHash = obj.cmdTermHash.append(res);
addHashMap(res,obj);
}
}
}
/* add to the remapTbl for hash lookup */
addHashMap(hash,obj) {
if(remapTbl.isKeyPresent(hash)) {
remapTbl[hash] = remapTbl[hash].append(obj);
} else {
remapTbl[hash] = [obj];
}
}
/* process a tokenized string: return new string, obj if deferred, or nil if no match */
processCmd(toks,tokcnt) {
local scmp = new StringComparator(nil,true,nil); // everything is lower case
local acmd = '', ahash, obj, v; // the string and its hash
/* build the command */
if(tokcnt > 0)
toks = toks.sublist(1,tokcnt);
acmd = toks.mapAll({x:x[1]});
acmd = acmd.join(' ');
ahash = scmp.calcHash(acmd);
if(!remapTbl.isKeyPresent(ahash))
return nil;
/* scan the items that fit */
foreach(obj in remapTbl[ahash]) {
// support list of locations???
if(obj.where != nil && valToList(obj.where).indexWhich({x:gLocation.isOrIsIn(x)}) == nil ) continue; // ECSE mod
if(!obj.when) continue; // ECSE mod
if(obj.during != nil && valToList(obj.during).indexWhich({s:s.isHappening}) == nil) continue; // ECSE mod
for(v = 1; v <= obj.cmdTermHash.length(); ++v) {
if(obj.cmdTermHash[v] == ahash &&
scmp.matchValues(obj.cmdTerms[v],acmd) != 0) {
// found it!
obj.doInsteadItems = nil; // clear it out before we move on
if(obj.remappedCmd != nil)
return obj.remappedCmd;
return obj;
}
}
}
return nil;
}
;
/* ------------------------------------------------------------------------ */
/*
* remapCmd tokenizer for US English. Other language modules should
* provide their own tokenizers to allow for differences in punctuation
* and other lexical elements.
*
*/
enum token tokOp;
/* Exception to handle exceptions occurring in the remapCmd tokenizer */
class EvalToksError: Exception
displayException() { "Evaluate tokenizer exception -- unable to parse"; }
;
/* Tokenizer for use with RemapCmd */
remapCmdTokenizer: Tokenizer
rules_ = static
[
/* skip whitespace */
['whitespace', R'<Space>+', nil, &tokCvtSkip, nil],
/*
* Words - note that we convert everything to lower-case. A
* word can start with any letter or number and then
* alphabetics, digits, hyphens, and apostrophes after that. (tokWord,tokString)
*/
['word', R'<AlphaNum>(<AlphaNum>|[-\'])*', tokWord, &tokCvtLower, nil],
// handle , as a separate item for conversations
['comma', ',', tokWord, nil, nil],
/*
* Single-quoted strings also allowed and only way to get empty string
*/
['string', R'\\?\'(<AlphaNum>|[-\'])*\\?\'', tokWord, &tokCvtStripSingle, nil],
// operators
['emptystring',R'(<vbar><Space>*<rparen>)|(<lparen><Space>*<vbar>)', tokOp, &tokDoEmptyString, nil],
['operator', R'[|()]', tokOp, nil, nil]
]
/* strip the leading and trailing single-quote -- and then push to lower-case */
tokCvtStripSingle(txt, typ, toks)
{
if(txt.startsWith('\\'))
txt = txt.substr(2);
if(txt.endsWith('\\'))
txt = txt.substr(1,txt.length() - 1);
local newlen = txt.length() - 2;
toks.append([txt.substr(2,newlen).toLower(), typ, txt]);
}
/* handle the |) or the (| sequence */
tokDoEmptyString(txt, typ, toks)
{
local len = txt.length();
toks.append([txt.substr(1,1),typ,txt.substr(1,1)]);
toks.append(['',tokWord,'']);
toks.append([txt.substr(len,1),typ,txt.substr(len,1)]);
}
;
//////////////////////////////////////////////////////////
// Define the remapCmd grammar for the parser
/* The most basic left level grammar map */
grammar remapCmdGrammar(lit): tokWord->txt_: Production
lstval() {
lst_ = [[txt_]];
return lst_;
}
;
/* handle concatenation of two words into the list
note the use of badness to prioritize the correct conversions */
grammar remapCmdGrammar(concat): [badness 50]
remapCmdGrammar->pp_ remapCmdGrammar->pp2_ : Production
lstval() {
return remapCmdGrammarConcat(pp_.lstval(),pp2_.lstval());
}
;
grammar remapCmdGrammar(or): [badness 30]
remapCmdGrammar->pp_ '|' remapCmdGrammar->pp2_ : Production
lstval() {
return remapCmdGrammarOr(pp_.lstval(),pp2_.lstval());
}
;
grammar remapCmdGrammar(tail): [badness 40]
remapCmdGrammar->pp_ remapCmdGrammar->pp2_ '|' remapCmdGrammar->pp3_: Production
lstval() {
local lst = remapCmdGrammarOr(pp2_.lstval(),pp3_.lstval());
return remapCmdGrammarConcat(pp_.lstval(),lst);
}
;
grammar remapCmdGrammar(grp): [badness 20]
'(' remapCmdGrammar->pp_ ')' : Production
lstval() {
return pp_.lstval();
}
;
/*
* Helper routines for the grammar text expansion
*/
remapCmdGrammarConcat(lstleft,lstright) {
local lft, rght;
local lst = [];
for(local i = 1; i <= lstleft.length(); ++i) {
for(local j = 1; j <= lstright.length(); ++j) {
lft = lstleft[i];
rght = lstright[j];
for(local k = 1; k <= rght.length(); ++k)
lft = lft.append(rght[k]);
lst = lst.append(lft);
}
}
return lst;
}
remapCmdGrammarOr(lst,lstright) {
for(local j = 1; j <= lstright.length(); ++j) {
lst = lst.append(lstright[j]);
}
return lst;
}
/////////////////////////////////////////////////////////////////////////////////
/* Modifications to the Parser to accommodate RemapCmd */
modify Parser
// return nil if an error; otherwise, return token list that could be empty
parseToksOnly(str)
{
/* Make sure our current SpecialVerb is set to nil before we start parsing a new command. */
specialVerbMgr.currentSV = nil;
/* tokenize the input */
local toks;
try
{
/* run the command tokenizer over the input string */
toks = cmdTokenizer.tokenize(str);
/* Dispose of any unwanted terminal punctuation */
while(toks.length > 0 && getTokType(toks[toks.length]) == tokPunct)
toks = toks.removeElementAt(toks.length);
}
catch (TokErrorNoMatch err)
{
/*
* The tokenizer found a character (usually a punctuation
* mark) that doesn't fit any of the token rules.
*/
DMsg(token error, 'I don\'t understand the punctuation {1}',
err.curChar_);
/* give up on the parse */
return nil;
}
return toks;
}
parse(str)
{
/* tokenize the input */
local toks = parseToksOnly(str);
if(toks == nil)
return;
/*
* Assume initially that the actor is the player character, but only
* if we don't have a question, since if the player is replying to a
* question the actor may already have been resolved.
*/
if(question == nil)
gActor = gPlayerChar;
/* no spelling corrections have been attempted yet */
local history = new transient SpellingHistory(self);
/* we're starting with the first command in the string */
local firstCmd = true;
/* parse the tokens */
try
{
/* if there are no tokens, simply perform the empty command */
if (toks.length() == 0)
{
/*
* this counts as a new command, so forget any previous
* question or typo information
*/
question = nil;
lastTokens = nil;
/* process an empty command */
emptyCommand();
/* we're done */
return;
}
/* check for an OOPS command */
local lst = oopsCommand.parseTokens(toks, cmdDict);
if (lst.length() != 0)
{
/* this only works if we have an error to correct */
local ui;
if (lastTokens == nil
|| (ui = spellingCorrector.findUnknownWord(lastTokens))
== nil)
{
/* OOPS isn't available - throw an error */
throw new CantOopsError();
}
/* apply the correction, and proceed to parse the result */
toks = OopsProduction.applyCorrection(lst[1], lastTokens, ui);
}
/* Update the vocabulary of any game objects with alternating/changing vocab. */
updateVocab();
/* Allow the specialVerb Manager to adjust our toks */
toks = specialVerbMgr.matchSV(toks);
/*
* Parse each predicate in the command line, until we run out
* of tokens. The beginning of a whole new command line is
* definitely the beginning of a sentence, so start parsing
* with firstCommandPhrase.
*/
for (local root = firstCommandPhrase ; toks.length() != 0 ; )
{
/* we don't have a parse list yet */
local cmdLst = nil;
local remapCmdItem = nil; // for later processing
local remapCmdTokCnt = 0;
/*
* we haven't found a resolution error in a non-command
* parsing yet
*/
local qErr = nil, defErr = nil;
/*
* If we have an outstanding question, and it takes
* priority over interpreting input as a new command, try
* parsing the input against the question. Only do this
* on the first command on the line - a question answer
* has to be the entire input, so if we've already parsed
* earlier commands on the same line, this definitely
* isn't an answer to a past question.
*/
if (firstCmd && question != nil && question.priority)
{
/* try parsing against the Question */
local l = question.parseAnswer(toks, cmdDict);
/* if it parsed and resolved, this is our command */
if (l != nil && l.cmd != nil)
cmdLst = l;
/* if it parsed but didn't resolved, note the error */
if (l != nil)
qErr = l.getResErr();
} else {
remapCmdTokCnt = 0;
while(++remapCmdTokCnt <= toks.length()) {
if(toks[remapCmdTokCnt][1] == ';' ||
toks[remapCmdTokCnt][1] == '.') {
break;
}
}
// if i is 1, then semicolon is the (empty) command!
if(--remapCmdTokCnt > 0) {
if(remapCmdTokCnt == toks.length())
remapCmdTokCnt = 0;
remapCmdItem = remapCmdDicts.processCmd(toks,remapCmdTokCnt);
if(dataType(remapCmdItem) == TypeSString) {
local remaptoks = parseToksOnly(remapCmdItem);
if(remaptoks == nil)
return;
local i = remapCmdTokCnt;
remapCmdTokCnt = remaptoks.length();
// rip out old tokens and replace with new ones!
if(i > 0) {
while(++i <= toks.length())
remaptoks = remaptoks.append(toks[i]);
}
// readjust the token set
toks = remaptoks;
remapCmdItem = nil;
}
}
}
/*
* if the question didn't grab it, try parsing as a whole
* new command against the ordinary command grammar
*/
if (cmdLst == nil || cmdLst.cmd == nil)
{
if(remapCmdItem == nil) {
cmdLst = new CommandList(
root, toks, cmdDict, { p: new Command(p) });
} else {
remapCmdItem.execute_(); // ECSE mod
if(remapCmdItem.doInsteadItems != nil) {
// create the artificial command
local cobj = remapCmdItem.doInsteadItems;
local c2;
if(cobj[2] == nil)
c2 = new Command(cobj[1]);
else if(cobj[3] == nil)
c2 = new Command(cobj[1],cobj[2]);
else if(cobj[4] == nil)
c2 = new Command(cobj[1],cobj[2],cobj[3]);
else
c2 = new Command(cobj[1],cobj[2],cobj[3],cobj[4]);
// fix c2 for items needed later here
c2.endOfSentence = true; // remapped commands are stand-alone
c2.nextTokens = remapCmdTokCnt > 0?
toks.sublist(remapCmdTokCnt+2) : [];
cmdLst = new CommandList(c2);
} else {
firstCmd = nil;
/* start over with a new spelling correction history */
history = new transient SpellingHistory(self);
// since we remapped, it will always be end-of-sentence
root = firstCommandPhrase;
/*
* Set the root grammar production for the next
* predicate. If the previous command ended the
* sentence, start a new sentence; otherwise, use the
* additional clause syntax.
*/
// root = cmd.endOfSentence
// ? firstCommandPhrase : commandPhrase;
//
/* go back and parse the remainder of the command line */
/* start index is 1 and have to skip the semi-colon as well */
if(remapCmdTokCnt > 0)
toks = toks.sublist(remapCmdTokCnt+2);
else
toks = [];
continue;
}
}
}
/*
* If we didn't find any resolvable commands, and this is
* the first command, check to see if it's an answer to
* an outstanding query. We only check this if the
* regular grammar parsing fails, because anything that
* looks like a valid new command overrides a past query.
* This is important because some of the short, common
* commands sometimes can look like noun phrases, so we
* explicitly give preference to interpreting these as
* brand new commands.
*/
if (cmdLst.cmd == nil
&& firstCmd
&& question != nil
&& !question.priority)
{
/* try parsing against the Question */
local l = question.parseAnswer(toks, cmdDict);
/* if it parsed and resolved, this is our command */
if (l != nil && l.cmd != nil)
cmdLst = l;
/* if it parsed but didn't resolved, note the error */
if (l != nil)
qErr = l.getResErr();
}
/*
* If we don't have a command yet, and this is the first
* command on the line, handle it as a conversational command
* if conversation is in progress; otherwise if default
* actions are enabled, check to see if the command looks like
* a single noun phrase. If so, handle it as the default
* action on the noun.
*/
if (cmdLst.cmd == nil
&& firstCmd)
{
local l;
/*
* If a conversation is in progress parse the command line
* as the single topic object phrase of a Say command,
* provided that the first word on the command line
* doesn't match a possible action.
*/
if(gPlayerChar.currentInterlocutor != nil
&& cmdLst.length == 0
&& Q.canTalkTo(gPlayerChar,
gPlayerChar.currentInterlocutor)
&& str.find(',') == nil
&& gPlayerChar.currentInterlocutor.allowImplicitSay())
{
l = new CommandList(
topicPhrase, toks, cmdDict,
{ p: new Command(SayAction, p) });
libGlobal.lastCommandForUndo = str;
savepoint();
}
/*
* If the player char is not in conversation with anyone,
* and the first word of the command doesn't match a possible
* command verb, then try parsing the command line as a
* single direct object phrase for the DefaultAction verb,
* provided defaultActions are enabled (which they are
* by default).
*/
else if(defaultActions)
l = new CommandList(
defaultCommandPhrase, toks, cmdDict,
{ p: new Command(p) });
/* accept a curable reply */
if (l != nil && l.acceptCurable() != nil)
{
cmdLst = l;
/* note any resolution error */
defErr = l.getResErr();
}
}
/*
* If we've applied a spelling correction, and the
* command match didn't consume the entire input, make
* sure what's left of the input has a valid parsing as
* another command. This ensures that we don't get a
* false positive by excessively shortening a command,
* which we can sometimes do by substituting a word like
* "then" for another word.
*/
if (cmdLst.length() != nil
&& history.hasCorrections())
{
/* get the best available parsing */
local c = cmdLst.getBestCmd();
/* if it doesn't use all the tokens, check what's left */
if (c != nil && c.tokenLen < toks.length())
{
/* try parsing the next command */
local l = commandPhrase.parseTokens(
c.nextTokens, cmdDict);
/*
* if that didn't work, invalidate the command by
* substituting an empty command list
*/
if (l.length() == 0)
cmdLst = new CommandList();
}
}
/*
* If we didn't find a parsing at all, it's a generic "I
* don't understand" error. If we found a parsing, but
* not a resolution, reject it if it's a spelling
* correction. We only want completely clean spelling
* corrections, without any errors.
*/
if (cmdLst.length() == 0
|| (history.hasCorrections()
&& cmdLst.getResErr() != nil
&& !cmdLst.getResErr().allowOnRespell))
{
/*
* If we were able to parse the input using one of
* the non-command interpretations, use the
* resolution error from that parsing. Otherwise, we
* simply can't make any sense of this input, so use
* the generic "I don't understand" error.
*/
local err = (qErr != nil ? qErr :
defErr != nil ? defErr :
new NotUnderstoodError());
/* look for a spelling correction */
local newToks = history.checkSpelling(toks, err);
if (newToks != nil)
{
/* parse again with the new tokens */
toks = newToks;
continue;
}
/*
* There's no spelling correction available. If we've
* settled on an auto-examine or question error, skip
* that and go back to "I don't understand" after
* all. We don't want to assume Auto-Examine unless we
* actually have something to examine, since we can
* parse noun phrase grammar out of practically any
* input.
*/
if (err is in (defErr, qErr))
{
/* return to the not-understood error */
err = new NotUnderstoodError();
/* check spelling again with this error */
newToks = history.checkSpelling(toks, err);
if (newToks != nil)
{
/* parse again with the new tokens */
toks = newToks;
continue;
}
/*
* We didn't find any spelling corrections this time
* through. Since we're rolling back to the
* not-understood error, discard any spelling
* corrections we attempted with other
* interpretations.
*/
history.clear();
}
/* fail with the error */
throw err;
}
/* if we found a resolvable command, execute it */
if (cmdLst.cmd != nil)
{
/* get the winning Command */
local cmd = cmdLst.cmd;
/*
* We next have to ensure that the player hasn't entered
* multiple nouns in a slot that only allows a single noun
* in the grammar. If the player has entered two objects
* like "the bat and the ball" in such a case, the
* badMulti flag will be set on the command object, so we
* first test for that and abort the command with a
* suitable error message if badMulti is not nil (by
* throwing a BadMultiError
*
* Unfortunately the badMulti flag doesn't get set if the
* player enters a multiple object as a plural (e.g.
* "bats"), so we need to trap this case too. We do that
* by checking whether there's multiple objects in the
* direct, indirect and accessory object slots at the same
* time as the grammar tag matching the slot in question
* is 'normal', which it is only for a single noun match.
*/
if(cmd && cmd.verbProd != nil &&
(cmd.badMulti != nil
|| (cmd.verbProd.dobjMatch != nil &&
cmd.verbProd.dobjMatch.grammarTag == 'normal'
&& cmd.dobjs.length > 1)
||
(cmd.verbProd.iobjMatch != nil &&
cmd.verbProd.iobjMatch.grammarTag == 'normal'
&& cmd.iobjs.length > 1)
||
(cmd.verbProd.accMatch != nil &&
cmd.verbProd.accMatch.grammarTag == 'normal'
&& cmd.accs.length > 1)
))
cmd.cmdErr = new BadMultiError(cmd.np);
/* if this command has a pending error, throw it */
if (cmd.cmdErr != nil)
throw cmd.cmdErr;
/*
* Forget any past question and typo information.
* The new command is either an answer to this
* question, or it's simply ignoring the question; in
* either case, the question is no longer in play for
* future input.
*/
question = nil;
lastTokens = nil;
/* note any spelling changes */
history.noteSpelling(toks);
/* execute the command */
cmd.exec();
/* start over with a new spelling correction history */
history = new transient SpellingHistory(self);
/*
* Set the root grammar production for the next
* predicate. If the previous command ended the
* sentence, start a new sentence; otherwise, use the
* additional clause syntax.
*/
root = cmd.endOfSentence
? firstCommandPhrase : commandPhrase;
/* we're no longer on the first command in the string */
firstCmd = nil;
/* go back and parse the remainder of the command line */
toks = cmd.nextTokens;
continue;
}
/*
* We weren't able to resolve any of the parse trees. If
* one of the errors is "curable", meaning that the
* player can fix it by answering a question, pick the
* first of those, in predicate priority order.
* Otherwise, just pick the first command overall in
* predicate priority order. In either case, since we
* didn't find any working alternatives, it's time to
* actually show the error and fail the command.
*/
local c = cmdLst.acceptAny();
/*
* If the error isn't curable, check for spelling errors,
* time permitting. Don't bother doing this with a
* curable error, since that will have its own way of
* solving the problem that reflects a better
* understanding of the input than considering it a
* simple typo.
*/
if (!c.cmdErr.curable)
{
/*
* For spelling correction purposes, if this is an
* unmatched noun error, but the command has a misc
* word list and an empty noun phrase, treat this as
* a "not understood" error. The combination of noun
* phrase errors suggests that we took a word that
* was meant to be part of the verb, and incorrectly
* parsed it as part of a noun phrase, leaving the
* verb structure and other noun phrase incomplete.
* This is really a verb syntax error, not a noun
* phrase error.
*/
local spellErr = c.cmdErr;
if (c.cmdErr.ofKind(UnmatchedNounError)
&& c.miscWordLists.length() > 0
&& c.missingNouns > 0)
spellErr = new NotUnderstoodError();
/* try spelling correction */
local newToks = history.checkSpelling(toks, spellErr);
/* if that worked, try the corrected command */
if (newToks != nil)
{
/* parse again with the new tokens */
toks = newToks;
continue;
}
}
/* re-throw the error that caused the resolution to fail */
throw c.cmdErr;
}
}
catch (ParseError err)
{
/*
* roll back any spelling changes to the last one that
* improved matters
*/
local h = history.rollback(toks, err);
toks = h.oldToks;
err = h.parseError;
/*
* if this is a curable error, it poses a question, which the
* player can answer on the next input
*/
if (err.curable)
question = new ParseErrorQuestion(err);
/*
* If the current error isn't curable, and unknown word
* disclosure is enabled, and there's a word in the command
* that's not in the dictionary, replace the parsing error
* with an unknown word error.
*/
local ui;
if (!err.curable
&& showUnknownWords
&& (ui = spellingCorrector.findUnknownWord(toks)) != nil)
{
/* find the misspelled word in the original tokens */
err = new UnknownWordError(getTokOrig(toks[ui]));
}
/*
* If the new error isn't an error in an OOPS command, save
* the token list for an OOPS command next time out.
*/
if (!err.ofKind(OopsError))
lastTokens = toks;
/* log any spelling changes we kept */
history.noteSpelling(toks);
/* display the error we finally decided upon */
err.display();
}
catch (CommandSignal sig)
{
/*
* On any command signal we haven't caught so far, simply
* stop processing this command line.
*/
}
}
;
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////
/*
* This is only emabled for testing the tokenizer and parser if you want to see how it
* operates. You will need to run tok_test2() from somewhere (I usually put it in
* showIntro for quick/easy/immediate access
*/
#if 0
tok_test2()
{
local tmap = new LookupTable([tokWord,'word',tokString,'string',tokOp,'operator']);
// get grammar info
// local ginfo = remapCmdGrammar.getGrammarInfo();
// local val = spelledToInt('ninety-six'); // to see how it is done
"Enter text to tokenize. Type Q or QUIT when done. ";
for (;;)
{
local str, toks, tok, match, res, c, lsthold;
/* read a string */
"\b>";
str = inputLine().split(';');
lsthold = [];
foreach(c in str) {
try {
toks = remapCmdTokenizer.tokenize(c);
//getTokVal(tok) returns the parsed value of the token.
//getTokType(tok) returns the type of the token.
//getTokOrig(tok) returns the original source text the token matched.
/* display the tokens */
for (local i = 1, local cnt = toks.length() ; i <= cnt ; ++i) {
tok = toks[i];
"(<<getTokVal(tok)>>,<<tmap[getTokType(tok)]>>)";
}
"\n";
// this phrase is set -- parse it up
match = remapCmdGrammar.parseTokens(toks,nil);
if(match.length > 0) {
res = match[1].lstval();
dumpList(res); "\n";
lsthold = remapCmdGrammarOr(lsthold,res);
}
else
"?????\n";
"-----\n";
}
catch (TokErrorNoMatch err)
{
"Unrecognized punctuation: <<err.remainingStr_.substr(1, 1)>>";
}
}
"------ Final -----\n";
dumpList(lsthold);
}
}
// simple list dumper
dumpList(lst) {
if(lst == nil)
"<nil>";
else {
"[";
local prefix = '';
foreach(local item in lst) {
"<<prefix>>";
prefix = ',';
if(dataType(item) == TypeList)
dumpList(item);
else if(item == ',')
"(comma)";
else {
item = toString(item);
item = item.findReplace(',','(comma)',ReplaceAll);
"<<item>>";
}
}
"]";
}
}
getDataType(data) {
local s = '?';
switch(dataType(data)) {
case TypeObject: s='object'; break;
case TypeList: s='list'; break;
case TypeSString: s='single-string'; break;
case TypeInt: s='integer'; break;
case TypeFuncPtr: s='func-ptr'; break;
case TypeProp: s='property'; break;
case TypeNil: s='nil'; break;
case TypeTrue: s='true'; break;
case TypeEnum: s='enum'; break;
// grammars
case GramTokTypeProd: s='gramProd'; break;
case GramTokTypeSpeech: s='gramSpeech'; break;
case GramTokTypeNSpeech: s='gramNSpeech'; break;
case GramTokTypeLiteral: s='gramLiteral'; break;
case GramTokTypeTokEnum: s='gramEnum'; break;
case GramTokTypeStar: s='gramStar'; break;
}
return s;
}
#endif
Adv3Lite Library Reference Manual
Generated on 08/12/2025 from adv3Lite version 2.2.2